package com.gitee.huxing.course.netty.codec;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.CombinedChannelDuplexHandler;
import io.netty.handler.codec.PrematureChannelClosureException;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.HttpClientUpgradeHandler.SourceCodec;
import io.netty.util.ReferenceCountUtil;

import java.util.ArrayDeque;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicLong;

/**
 * @Description: http客户端编码器
 * @ProjectName: JavaCourse
 * @Package: com.gitee.huxing.course.netty.codec
 * @ClassName: HttpServerCustomCodec
 * @Author: huxingmale
 * @DateTime: 2021-08-22 16:57
 */

public final class HttpClientCustomCodec extends CombinedChannelDuplexHandler<HttpResponseDecoder, HttpRequestEncoder> implements SourceCodec {

    public static final boolean DEFAULT_FAIL_ON_MISSING_RESPONSE = false;

    public static final boolean DEFAULT_PARSE_HTTP_AFTER_CONNECT_REQUEST = false;

    private final Queue<HttpMethod> queue;

    private final boolean parseHttpAfterConnectRequest;

    private boolean done;

    private final AtomicLong requestResponseCounter;

    private final boolean failOnMissingResponse;

    public HttpClientCustomCodec() {
        this(4096, 8192, 8192, false);
    }

    public HttpClientCustomCodec(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize) {
        this(maxInitialLineLength, maxHeaderSize, maxChunkSize, false);
    }

    public HttpClientCustomCodec(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse) {
        this(maxInitialLineLength, maxHeaderSize, maxChunkSize, failOnMissingResponse, true);
    }

    public HttpClientCustomCodec(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse, boolean validateHeaders) {
        this(maxInitialLineLength, maxHeaderSize, maxChunkSize, failOnMissingResponse, validateHeaders, false);
    }

    public HttpClientCustomCodec(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse, boolean validateHeaders, boolean parseHttpAfterConnectRequest) {
        this.queue = new ArrayDeque();
        this.requestResponseCounter = new AtomicLong();
        this.init(new HttpClientCustomCodec.Decoder(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders), new HttpClientCustomCodec.Encoder());
        this.failOnMissingResponse = failOnMissingResponse;
        this.parseHttpAfterConnectRequest = parseHttpAfterConnectRequest;
    }

    public HttpClientCustomCodec(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse, boolean validateHeaders, int initialBufferSize) {
        this(maxInitialLineLength, maxHeaderSize, maxChunkSize, failOnMissingResponse, validateHeaders, initialBufferSize, false);
    }

    public HttpClientCustomCodec(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse, boolean validateHeaders, int initialBufferSize, boolean parseHttpAfterConnectRequest) {
        this(maxInitialLineLength, maxHeaderSize, maxChunkSize, failOnMissingResponse, validateHeaders, initialBufferSize, parseHttpAfterConnectRequest, false);
    }

    public HttpClientCustomCodec(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse, boolean validateHeaders, int initialBufferSize, boolean parseHttpAfterConnectRequest, boolean allowDuplicateContentLengths) {
        this.queue = new ArrayDeque();
        this.requestResponseCounter = new AtomicLong();
        this.init(new HttpClientCustomCodec.Decoder(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders, initialBufferSize, allowDuplicateContentLengths), new HttpClientCustomCodec.Encoder());
        this.parseHttpAfterConnectRequest = parseHttpAfterConnectRequest;
        this.failOnMissingResponse = failOnMissingResponse;
    }

    public void prepareUpgradeFrom(ChannelHandlerContext ctx) {
        ((HttpClientCustomCodec.Encoder)this.outboundHandler()).upgraded = true;
    }

    public void upgradeFrom(ChannelHandlerContext ctx) {
        ChannelPipeline p = ctx.pipeline();
        p.remove(this);
    }

    public void setSingleDecode(boolean singleDecode) {
        ((HttpResponseDecoder)this.inboundHandler()).setSingleDecode(singleDecode);
    }

    public boolean isSingleDecode() {
        return ((HttpResponseDecoder)this.inboundHandler()).isSingleDecode();
    }

    private final class Decoder extends HttpResponseDecoder {
        Decoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean validateHeaders) {
            super(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders);
        }

        Decoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean validateHeaders, int initialBufferSize, boolean allowDuplicateContentLengths) {
            super(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders, initialBufferSize, allowDuplicateContentLengths);
        }

        protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception {
            int oldSize;
            if (HttpClientCustomCodec.this.done) {
                oldSize = this.actualReadableBytes();
                if (oldSize == 0) {
                    return;
                }

                out.add(buffer.readBytes(oldSize));
            } else {
                oldSize = out.size();
                super.decode(ctx, buffer, out);
                if (HttpClientCustomCodec.this.failOnMissingResponse) {
                    int size = out.size();

                    for(int i = oldSize; i < size; ++i) {
                        this.decrement(out.get(i));
                    }
                }
            }

        }

        private void decrement(Object msg) {
            if (msg != null) {
                if (msg instanceof LastHttpContent) {
                    HttpClientCustomCodec.this.requestResponseCounter.decrementAndGet();
                }

            }
        }

        protected boolean isContentAlwaysEmpty(HttpMessage msg) {
            HttpMethod method = (HttpMethod)HttpClientCustomCodec.this.queue.poll();
            int statusCode = ((HttpResponse)msg).status().code();
            if (statusCode >= 100 && statusCode < 200) {
                return super.isContentAlwaysEmpty(msg);
            } else {
                if (method != null) {
                    char firstChar = method.name().charAt(0);
                    switch(firstChar) {
                        case 'C':
                            if (statusCode == 200 && HttpMethod.CONNECT.equals(method)) {
                                if (!HttpClientCustomCodec.this.parseHttpAfterConnectRequest) {
                                    HttpClientCustomCodec.this.done = true;
                                    HttpClientCustomCodec.this.queue.clear();
                                }

                                return true;
                            }
                            break;
                        case 'H':
                            if (HttpMethod.HEAD.equals(method)) {
                                return true;
                            }
                    }
                }

                return super.isContentAlwaysEmpty(msg);
            }
        }

        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            super.channelInactive(ctx);
            if (HttpClientCustomCodec.this.failOnMissingResponse) {
                long missingResponses = HttpClientCustomCodec.this.requestResponseCounter.get();
                if (missingResponses > 0L) {
                    ctx.fireExceptionCaught(new PrematureChannelClosureException("channel gone inactive with " + missingResponses + " missing response(s)"));
                }
            }

        }
    }

    private final class Encoder extends HttpRequestEncoder {
        boolean upgraded;

        private Encoder() {
        }

        protected void encode(ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception {
            if (this.upgraded) {
                out.add(ReferenceCountUtil.retain(msg));
            } else {
                if (msg instanceof HttpRequest) {
                    HttpClientCustomCodec.this.queue.offer(((HttpRequest)msg).method());
                }

                super.encode(ctx, msg, out);
                if (HttpClientCustomCodec.this.failOnMissingResponse && !HttpClientCustomCodec.this.done && msg instanceof LastHttpContent) {
                    HttpClientCustomCodec.this.requestResponseCounter.incrementAndGet();
                }
            }
        }
    }
}
